iT邦幫忙

2025 iThome 鐵人賽

DAY 3
1

🎯 今天要做什麼?

昨天我們深入了解了斷言的各種用法,今天要學習 TDD 的精髓 —「紅綠重構循環」。

想像一下,你接到一個需求:「我們需要一個判斷質數的函數。」以前你可能直接開始寫程式,但現在我們要用 TDD 的方式:先寫測試(紅燈),再寫最簡實作(綠燈),最後改善代碼(重構)。

學習目標

今天結束後,你將學會:

  • 深度理解 TDD 的三個階段:紅燈、綠燈、重構
  • 掌握每個階段的具體操作和心態
  • 體驗完整的 TDD 開發節奏
  • 學會基本重構手法

TDD 循環的核心理念

TDD 的核心是一個簡單而強大的三步循環:

🔴 紅燈(Red)   ➜  🟢 綠燈(Green)  ➜  🔵 重構(Refactor)
  ↑                                           ↓
  ←  ←  ←  ←  ←  ←  ←  ←  ←  ←  ←  ←  ←  ←  ←
  • 🔴 紅燈階段:寫失敗的測試,表達期望的行為
  • 🟢 綠燈階段:用最簡單的方法讓測試通過
  • 🔵 重構階段:在測試保護下改善代碼品質

🔴 紅燈階段:寫失敗的測試

紅燈階段的核心思想:先思考需求,再動手寫程式

建立 tests/Unit/Day03/MathUtilsTest.php

<?php

describe('Math Utilities', function () {
    describe('isPrime function', function () {
        it('identifies small prime numbers', function () {
            // 還沒有 isPrime 函數,所以這個測試會失敗(紅燈)
            expect(isPrime(2))->toBe(true);
            expect(isPrime(3))->toBe(true);
            expect(isPrime(5))->toBe(true);
        });

        it('identifies small composite numbers', function () {
            expect(isPrime(4))->toBe(false);
            expect(isPrime(6))->toBe(false);
            expect(isPrime(9))->toBe(false);
        });
    });
});

執行測試:

php artisan test tests/Unit/Day03

預期結果:測試失敗,因為 isPrime 函數還不存在。這就是我們要的「紅燈」!

🟢 綠燈階段:最快速度讓測試通過

綠燈階段的核心思想:用最簡單的方法讓測試通過

建立 app/Math/MathUtils.php

<?php

namespace App\Math;

class MathUtils
{
    public static function isPrime(int $n): bool
    {
        // 最簡單的實作:硬編碼我們測試的數字
        if (in_array($n, [2, 3, 5])) {
            return true;
        }
        if (in_array($n, [4, 6, 9])) {
            return false;
        }
        return false; // 其他數字先回傳 false
    }
}

建立測試輔助函數 tests/Helpers/functions.php

<?php
use App\Math\MathUtils;

if (!function_exists('isPrime')) {
    function isPrime(int $n): bool {
        return MathUtils::isPrime($n);
    }
}

tests/TestCase.php 中引入這個檔案。

執行測試:

php artisan test tests/Unit/Day03

結果:測試通過!我們達到了綠燈階段。

🔵 重構階段:改善代碼品質

重構階段的核心思想:在測試保護下,改善代碼品質

我們的硬編碼實作太醜了,讓我們重構:

<?php

namespace App\Math;

class MathUtils
{
    public static function isPrime(int $n): bool
    {
        // 處理邊界情況
        if ($n < 2) return false;
        if ($n === 2) return true;
        if ($n % 2 === 0) return false;
        
        // 檢查奇數因子到 sqrt(n)
        for ($i = 3; $i * $i <= $n; $i += 2) {
            if ($n % $i === 0) return false;
        }
        
        return true;
    }
}

執行測試確認重構成功:

php artisan test tests/Unit/Day03

測試仍然通過!重構成功。

完整循環實戰演練

讓我們做第二輪循環,增加邊界情況的測試:

it('handles edge cases', function () {
    expect(isPrime(0))->toBe(false);
    expect(isPrime(1))->toBe(false);
});

it('handles larger prime numbers', function () {
    expect(isPrime(11))->toBe(true);
    expect(isPrime(13))->toBe(true);
});

執行測試 - 全部通過!因為我們的重構實作已經正確處理了這些情況。

TDD 開發節奏

TDD 不只是技術,更是一種開發節奏:

  1. 小步快跑:每次只測試一個小功能
  2. 快速反饋:幾分鐘完成一個循環
  3. 持續驗證:每次修改都有測試保護
  4. 漸進改善:通過重構持續提升品質

心理建設要點

  • 紅燈不可怕:失敗指引方向
  • 綠燈要克制:最簡實作,避免過度工程
  • 重構要勇敢:有測試護航,放心改善

重構時機的判斷

看到這些「代碼異味」就該重構了:

  • 重複代碼:相同邏輯出現多次
  • 過長函數:函數做太多事情
  • 魔術數字:硬編碼的數值
  • 不清楚的命名:變數或函數名不明確

常見的重構手法

1. 提取常數

重構前:

if ($age >= 18) { /* ... */ }

重構後:

const MIN_ADULT_AGE = 18;
if ($age >= self::MIN_ADULT_AGE) { /* ... */ }

2. 重命名變數

重構前:

function calc($x) { return $x * 0.1; }

重構後:

function calculateTax(float $price): float {
    const TAX_RATE = 0.1;
    return $price * self::TAX_RATE;
}

💡 Pest 的 TDD 優勢

Pest 讓 TDD 變得更簡潔、更直觀:

鏈式語法更流暢

it('handles prime numbers', function () {
    expect(isPrime(7))->toBe(true);
});

描述性測試結構

describe('Math Utilities', function () {
    describe('isPrime function', function () {
        // 測試邏輯分組清晰
    });
});

更好的可讀性

與傳統 PHPUnit 相比,Pest 的語法更像在描述需求而非寫程式碼。這讓我們在 TDD 的紅燈階段更容易專注於「需求是什麼」,而不是「怎麼寫測試」。

完整實作範例

完整 app/Math/MathUtils.php

<?php

namespace App\Math;

class MathUtils
{
    public static function isPrime(int $n): bool
    {
        if ($n < 2) return false;
        if ($n === 2) return true;
        if ($n % 2 === 0) return false;
        
        for ($i = 3; $i * $i <= $n; $i += 2) {
            if ($n % $i === 0) return false;
        }
        
        return true;
    }
}

完整 tests/Unit/Day03/MathUtilsTest.php

<?php

describe('Math Utilities', function () {
    describe('isPrime function', function () {
        it('identifies small prime numbers', function () {
            expect(isPrime(2))->toBe(true);
            expect(isPrime(3))->toBe(true);
            expect(isPrime(5))->toBe(true);
        });

        it('identifies small composite numbers', function () {
            expect(isPrime(4))->toBe(false);
            expect(isPrime(6))->toBe(false);
            expect(isPrime(9))->toBe(false);
        });

        it('handles edge cases', function () {
            expect(isPrime(0))->toBe(false);
            expect(isPrime(1))->toBe(false);
        });

        it('handles larger prime numbers', function () {
            expect(isPrime(11))->toBe(true);
            expect(isPrime(13))->toBe(true);
        });
    });
});

完整 tests/Helpers/functions.php

<?php
use App\Math\MathUtils;

if (!function_exists('isPrime')) {
    function isPrime(int $n): bool {
        return MathUtils::isPrime($n);
    }
}

今天學到什麼?

技術收穫

  • 掌握 TDD 三階段:紅燈寫測試、綠燈快速實作、重構改善品質
  • 理解各階段心態:紅燈專注需求、綠燈專注通過、重構專注品質
  • 體驗開發節奏:小步快跑、快速反饋、持續改善
  • 學會基本重構:提取常數、重命名變數

關鍵要點

  • 紅燈是正常的:失敗的測試指引方向
  • 綠燈要克制:最簡實作就好,不要想太複雜
  • 重構很安全:有測試保護,可以放心改善代碼
  • 小步快跑:每次只改一點點,頻繁執行測試

🎉 總結

TDD 的紅綠重構循環看似簡單,但要真正掌握需要大量練習。它不只是技術方法,更是一種思維模式的轉變。

今日小挑戰 🏆

試著用 TDD 方式實作一個 isEven 函數:

  1. 先寫測試(什麼數字是偶數?)
  2. 最簡實作(讓測試通過)
  3. 重構改善(讓代碼更優雅)

記住 TDD 的節奏:紅燈 → 綠燈 → 重構,小步快跑!

明天我們將學習「測試結構和組織」,了解如何讓測試更清晰、更好維護。


上一篇
Day 02 - 認識斷言(Assertions) 🚀
下一篇
Day 04 - 測試結構與組織 🚀
系列文
Laravel Pest TDD 實戰:從零開始的測試驅動開發9
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言